/*
 * XMLDBStorage.java
 *
 * Created on April 27, 2007, 8:22 AM
 *
 * To change this template, choose Tools | Template Manager
 * and open the template in the editor.
 */

package org.atomojo.app.storage.xmldb;

import java.io.File;
import java.io.FileOutputStream;
import org.atomojo.app.*;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.io.Writer;
import java.net.URLEncoder;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.UUID;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.atomojo.app.db.Entry;
import org.exist.restlet.XMLDB;
import org.exist.restlet.XMLDBResource;
import org.infoset.xml.Document;
import org.infoset.xml.DocumentLoader;
import org.infoset.xml.ItemDestination;
import org.infoset.xml.XMLException;
import org.infoset.xml.util.WriterItemDestination;
import org.infoset.xml.util.XMLWriter;
import org.restlet.Application;
import org.restlet.Context;
import org.restlet.Request;
import org.restlet.Response;
import org.restlet.Restlet;
import org.restlet.data.CharacterSet;
import org.restlet.data.MediaType;
import org.restlet.data.Method;
import org.restlet.data.Reference;
import org.restlet.data.Status;
import org.restlet.representation.InputRepresentation;
import org.restlet.representation.OutputRepresentation;
import org.restlet.representation.Representation;
import org.restlet.representation.StringRepresentation;
import org.restlet.routing.Router;

/**
 *
 * @author alex
 */
public class XMLDBStorage implements Storage
{
   public static final String FEED_DOCUMENT_NAME = ".feed.atom";
   
   class XQuery implements Query {
      Reference mediaRef;
      String query;
      XQuery(Reference ref) {
         this.mediaRef = ref;
         this.query = null;
      }
      XQuery(String query) {
         this.mediaRef = null;
         this.query = query;
      }
   }
   
   DocumentLoader loader;
   String dbName;
   XMLDB xmldb;
   File xmldbDir;
   Reference atomBase;
   boolean makeCollections;
   Context context;
   Map<String,Reference> queries;
   
   /** Creates a new instance of XMLDBStorage */
   public XMLDBStorage(File xmldbDir,String dbName,Reference atomBase,DocumentLoader loader) 
      throws IOException
   {
      this.loader = loader;
      this.xmldbDir = xmldbDir;
      this.atomBase = atomBase;
      this.makeCollections = false;
      this.dbName = dbName;
      this.queries = new HashMap<String,Reference>();
      File confFile = new File(xmldbDir,"conf.xml");
      File dbDir = xmldbDir.getParentFile();
      xmldb = new XMLDB(dbName,confFile);
      String log4j = System.getProperty("log4j.configuration");
      if (log4j == null) {
         File lf = new File(dbDir,"log4j.xml");
         if (!lf.exists()) {
            String [] resources = { "log4j.dtd", "log4j.xml" };
            String [] names = { "log4j.dtd", "log4j.xml" };
            for (int i=0; i<resources.length; i++) {
               File outFile = new File(dbDir.getParentFile(),names[i]);
               if (!outFile.exists()) {
                  copyResource(resources[i],outFile);
               }
            }
         }

         if (lf.canRead()) {
            System.setProperty("log4j.configuration", lf.toURI().toASCIIString());
         }
      }
   }

   protected Logger getLogger() {
      return context.getLogger();
   }
   
   public void start() 
      throws Exception
   {
      getLogger().info("Starting XMLDB "+dbName);
      if (!xmldbDir.exists()) {
         xmldbDir.mkdirs();
         writeXMLDBConfiguration();
      }
      xmldb.start();
      loadQueries();

      getLogger().info("XMLDB "+dbName+" started.");
   }

   protected Response put(Reference uri,Representation entity) {
      Request request = new Request(Method.PUT,uri);
      request.setEntity(entity);
      return context.getClientDispatcher().handle(request);
   }
   protected Response post(Reference uri,Representation entity) {
      Request request = new Request(Method.POST,uri);
      request.setEntity(entity);
      return context.getClientDispatcher().handle(request);
   }
   protected Response head(Reference uri) {
      Request request = new Request(Method.HEAD,uri);
      return context.getClientDispatcher().handle(request);
   }
   protected Response get(Reference uri) {
      Request request = new Request(Method.GET,uri);
      return context.getClientDispatcher().handle(request);
   }
   protected Response getWithQuery(Reference uri,Reference query) {
      Request request = new Request(Method.GET,uri);
      //context.getLogger().info("Query: "+XMLDBResource.XQUERY_ATTR+" = "+query);
      request.getAttributes().put(XMLDBResource.XQUERY_NAME, query);
      return context.getClientDispatcher().handle(request);
   }
   protected Response delete(Reference uri) {
      Request request = new Request(Method.DELETE,uri);
      return context.getClientDispatcher().handle(request);
   }
   protected void loadQuery(String name,Reference uri,String query) {
      //context.getLogger().info("Storing query: "+name+" -> "+uri);
      queries.put(name,uri);
      Response response = put(uri,new StringRepresentation(query,AtomResource.XQUERY_TYPE));
      if (!response.getStatus().isSuccess()) {
         String text = response.isEntityAvailable() ? response.getEntityAsText() : "";
         context.getLogger().log(Level.SEVERE,"Cannot store query "+query+", status="+response.getStatus().getCode()+", "+text);
      }
   }
   protected void loadQueries() {
      context.getLogger().info("base: "+atomBase);
      loadQuery("feed-head",new Reference(atomBase+"queries/feed-head.xq"),
         "declare namespace atom='http://www.w3.org/2005/Atom'; "+
         "let $cpath := substring-before(base-uri(/),'.feed.atom') return " +
         "<feed xml:base='./' xmlns='http://www.w3.org/2005/Atom' xmlns:app='"+AtomResource.APP_NAMESPACE+"'>\n{ (for $e in (xcollection($cpath)/atom:feed/*) return ($e,'&#xa;'))} </feed>"
      );
      loadQuery("feed-updated",new Reference(atomBase+"queries/feed-updated.xq"),
         "declare namespace atom='http://www.w3.org/2005/Atom'; declare variable $updated as xs:string external; update replace /atom:feed/atom:updated with <updated xmlns='http://www.w3.org/2005/Atom'>{$updated}</updated>"
      );
      loadQuery("feed-title",new Reference(atomBase+"queries/feed-title.xq"),
         "declare namespace atom='http://www.w3.org/2005/Atom'; /atom:feed/atom:title/text()"
      );
      loadQuery("entry-get",new Reference(atomBase+"queries/entry-get.xq"),
         "declare namespace atom='http://www.w3.org/2005/Atom'; "+
         "declare variable $base as xs:string external; "+
         "<entry xmlns='http://www.w3.org/2005/Atom' xmlns:app='"+AtomResource.APP_NAMESPACE+"'>\n{ ( attribute xml:base { $base }, for $e in (/atom:entry/*) return ($e,'&#xa;'))} </entry>"
      );

   }

   public void init(Context context)
   {
      this.context = context;
   }
   
   public void stop() 
      throws Exception
   {
      getLogger().info("Stopping XMLDB "+dbName);
      xmldb.stop();
      getLogger().info("XMLDB "+dbName+" stopped.");
   }

   public Application getAdministration() {
      return new Application(context.createChildContext()) {
         public Restlet createRoot() {
            getContext().getAttributes().put(XMLDBResource.DBNAME_NAME, dbName);
            Router router = new Router(getContext());
            Restlet reindexer = new Restlet(getContext()) {
               public void handle(Request request,Response response) {
                  if (request.getMethod()!=Method.GET) {
                     response.setStatus(Status.CLIENT_ERROR_METHOD_NOT_ALLOWED);
                     return;
                  }
                  try {
                     xmldb.reindex(request.getResourceRef().getRemainingPart());
                     response.setStatus(Status.SUCCESS_NO_CONTENT);
                  } catch (Exception ex) {
                     response.setStatus(Status.SERVER_ERROR_INTERNAL,"Cannot reindex collection due to exception.");
                     getLogger().log(Level.SEVERE,"Cannot reindex collection due to exception.",ex);
                  }
               }
            };
            router.attach("/data/",XMLDBResource.class);
            router.attach("/reindex",reindexer);
            return router;
         }
      };
   }
   
   protected void writeXMLDBConfiguration()
      throws java.io.IOException
   {
      File dataDir = new File(xmldbDir,"data");
      if (!dataDir.exists()) {
         dataDir.mkdir();
      }
      String [] resources = { "catalog.xml", "conf.xml" };
      String [] names = { "catalog.xml", "conf.xml" };
      for (int i=0; i<resources.length; i++) {
         File outFile = new File(xmldbDir,names[i]);
         if (!outFile.exists()) {
            copyResource(resources[i],outFile);
         }
      }
   }
   
   protected void copyResource(String resourcePath,File outFile)
      throws IOException
   {
      InputStream in = getClass().getResourceAsStream(resourcePath);
      if (in==null) {
         throw new IOException("Cannot open resource "+resourcePath);
      }
      FileOutputStream out = new FileOutputStream(outFile);
      byte [] buffer = new byte[8192];
      int len;
      while ((len=in.read(buffer))>0) {
         out.write(buffer,0,len);
      }
      out.close();
      in.close();
   }
   
   public Representation getFeed(final String path,UUID id,final Iterator<Entry> entries)
      throws IOException
   {
      Reference feedRef = makeFeedReference(path);
      if (getLogger().isLoggable(Level.FINE)) {
         getLogger().fine("Feed ref: "+feedRef);
      }
      Response response = get(feedRef);
      if (response.getStatus().isSuccess()) {
         final Representation feedRep = response.getEntity();
         Representation rep = new OutputRepresentation(MediaType.APPLICATION_ATOM) {
            boolean released = false;
            public void write(OutputStream os)
               throws IOException
            {
               Writer out = new OutputStreamWriter(os,"UTF-8");
               ItemDestination dest = new WriterItemDestination(out,"UTF-8");
               FeedLoader feedLoader = new FeedLoader(getLogger(),loader,feedRep,entries);
               try {
                  feedLoader.load(XMLDBStorage.this,context.getClientDispatcher(),path,dest);
               } catch (XMLException ex) {
                  throw new IOException("XML exception while loading feed: "+ex.getMessage());
               }
               feedRep.release();
               released = true;
               out.flush();
               out.close();
            }
            public void release() {
               if (!released) {
                  feedRep.release();
               }
            }
         };
         rep.setCharacterSet(CharacterSet.UTF_8);
         return rep;
      } else {
         throw new IOException("Cannot get feed document "+feedRef+", status="+response.getStatus().getCode());
      }
   }
   
   public Representation getFeedHead(String path,UUID id)
      throws IOException
   {
      Reference feedRef = makeFeedReference(path);
      if (getLogger().isLoggable(Level.FINE)) {
         getLogger().fine("Feed ref: "+feedRef);
      }
      Response response = getWithQuery(feedRef,queries.get("feed-head"));
      //Response response = client.get(feedRef);
      if (response.getStatus().isSuccess()) {
         Representation rep = response.getEntity();
         rep.setMediaType(MediaType.APPLICATION_ATOM);
         rep.setCharacterSet(CharacterSet.UTF_8);
         return rep;
      } else {
         boolean hasEntity = response.isEntityAvailable();
         String msg = hasEntity ? response.getEntity().getText() : "";
         if (hasEntity) {
            response.getEntity().release();
         }
         throw new IOException("Cannot get feed due to status "+response.getStatus()+": "+msg);
      }
   }
   
   public boolean feedUpdated(String path,UUID feedId,Date updated)
      throws IOException
   {
      String updatedS = AtomResource.toXSDDate(updated);
      Reference feedRef = makeFeedReference(path);
      if (context.getLogger().isLoggable(Level.FINE)) {
         context.getLogger().fine("Marking feed "+feedId+" updated at "+updatedS+" in XML DB "+dbName+" at "+feedRef);
      }
      feedRef.setQuery("updated="+updatedS);
      Response response = getWithQuery(feedRef,queries.get("feed-updated"));
      if (response.isEntityAvailable()) {
         response.getEntity().release();
      }
      return response.getStatus().isSuccess();
   }
   
   public String getFeedTitle(String path,UUID id) 
      throws IOException
   {
      Reference feedRef = makeFeedReference(path);
      Response response = getWithQuery(feedRef,queries.get("feed-title"));
      boolean release = response.isEntityAvailable();
      String value = response.getStatus().isSuccess() ? response.getEntity().getText() : null;
      if (release) {
         response.getEntity().release();
      }
      return value;
   }
   
   public Status storeFeed(String path,UUID id,Document doc)
   {
      Reference feedRef = makeFeedReference(path);
      InfosetRepresentation feedRep = new InfosetRepresentation(MediaType.APPLICATION_ATOM,doc);
      Response putResponse = put(feedRef,feedRep);
      if (putResponse.isEntityAvailable()) {
         putResponse.getEntity().release();
      }
      boolean success = putResponse.getStatus().isSuccess();
      if (!success) {
         String msg = "";
         try {
            Representation rep = putResponse.getEntity();
            if (rep!=null) {
               msg = rep.getText();
            }
         } catch (IOException ex) {
         }
         getLogger().log(Level.SEVERE,"Cannot store feed document "+feedRef+": "+msg);
      }
      return putResponse.getStatus();
      
   }
   
   public boolean deleteFeed(String path, UUID id)
   {
      Reference feedRef = makeFeedReference(path);
      Reference collection = feedRef.getParentRef();
      Response delResponse = delete(collection);
      boolean success = delResponse.getStatus().isSuccess();
      boolean hasEntity = delResponse.isEntityAvailable();
      if (!success) {
         String msg = "";
         try {
            Representation rep = delResponse.getEntity();
            if (rep!=null) {
               msg = rep.getText();
            }
         } catch (IOException ex) {
         }
         getLogger().log(Level.SEVERE,"Cannot delete feed document "+feedRef+": ("+delResponse.getStatus().getCode()+") "+msg);
      }
      if (hasEntity) {
         delResponse.getEntity().release();
      }
      return success;
   }
   
   public Status storeEntry(String path,UUID feedId,UUID id,Document entryDoc)
      throws IOException
   {
      Reference entryDocRef = makeEntryReference(path,id);
      //Reference entryDocRef = new Reference(feedRef.getParentRef()+"."+id.toString()+".atom");
      if (context.getLogger().isLoggable(Level.FINE)) {
         context.getLogger().fine("Creating entry "+id+" in "+feedId+" in XML DB "+dbName+" at "+entryDocRef);
      }
      
      String xml = null;
      try {
         StringWriter w = new StringWriter();
         XMLWriter.writeDocument(entryDoc,w);
         xml = w.toString();
         //log.info(xml);
      } catch (Exception ex) {
         // TODO: need to delete
         getLogger().log(Level.SEVERE,"Cannot serialize entry for storage.",ex);
         return Status.SERVER_ERROR_INTERNAL;
      }
      
      Representation srep = new StringRepresentation(xml,MediaType.APPLICATION_ATOM);
      srep.setCharacterSet(CharacterSet.UTF_8);
      Response response = put(entryDocRef,srep);
      boolean hasEntity = response.isEntityAvailable();
         
      boolean success = response.getStatus().isSuccess();
      if (!success) {
         Representation rep = response.getEntity();
         if (rep!=null) {
            try {
               getLogger().log(Level.SEVERE,"Cannot store entry: "+response.getStatus()+" "+response.getEntity().getText());
            } catch (IOException ex) {
               getLogger().log(Level.SEVERE,"Cannot store entry: "+response.getStatus());
            }
         }
      }
      if (hasEntity) {
         response.getEntity().release();
      }
      return response.getStatus();
      
   }
   
   public Representation getEntry(String feedBaseURI,String path,UUID feedId,UUID id)
      throws IOException
   {
      Reference entryDocRef = makeEntryReference(path,id);
      entryDocRef.setQuery("base="+feedBaseURI);
      //Response response = client.get(entryDocRef);
      Response response = getWithQuery(entryDocRef,queries.get("entry-get"));
      if (response.getStatus().isSuccess()) {
         Representation rep = response.getEntity();
         rep.setMediaType(MediaType.APPLICATION_ATOM);
         rep.setCharacterSet(CharacterSet.UTF_8);
         return rep;
      } else {
         boolean hasEntity = response.isEntityAvailable();
         String msg = hasEntity ? response.getEntity().getText() : "";
         if (hasEntity) {
            response.getEntity().release();
         }
         throw new IOException("Cannot get entry document "+entryDocRef+" due to status "+response.getStatus()+": "+msg);
      }
      
   }
   public boolean deleteEntry(String path,UUID feedId,UUID id)
   {
      Reference entryDocRef = makeEntryReference(path,id);
      Response delResponse = delete(entryDocRef);
      boolean success = delResponse.getStatus().isSuccess();
      boolean hasEntity = delResponse.isEntityAvailable();
      if (!success) {
         String msg = "";
         try {
            Representation rep = delResponse.getEntity();
            msg = rep==null ? "" : rep.getText();
         } catch (IOException ex) {
         }
         getLogger().log(Level.SEVERE,"Cannot delete entry document ("+delResponse.getStatus().getCode()+") "+entryDocRef+": "+msg);
      }
      if (hasEntity) {
         delResponse.getEntity().release();
      }
      return success || delResponse.getStatus().getCode()==404;
   }
   
   public Status storeMedia(String path,UUID feedId,String name,MediaType type,InputStream data)
      throws IOException
   {
      Reference mediaRef = makeMediaReference(path,name);
      
      Response response = put(mediaRef,new InputRepresentation(data,type));
         
      boolean success = response.getStatus().isSuccess();
      boolean hasEntity = response.isEntityAvailable();
      if (!success) {
         Representation rep = response.getEntity();
         if (rep!=null) {
            try {
               getLogger().log(Level.SEVERE,"Cannot store media: "+response.getStatus()+" "+response.getEntity().getText());
            } catch (IOException ex) {
               getLogger().log(Level.SEVERE,"Cannot store media: "+response.getStatus());
            }
         }
      }
      if (hasEntity) {
         response.getEntity().release();
      }
      return response.getStatus();
   }
   
   public Representation getMedia(String path,UUID feedId,String name)  
      throws IOException
   {
      Reference mediaRef = makeMediaReference(path,name);
      Response response = get(mediaRef);
      if (response.getStatus().isSuccess()) {
         return response.getEntity();
      } else {
         boolean hasEntity = response.isEntityAvailable();
         String msg = hasEntity ? response.getEntity().getText() : "";
         if (hasEntity) {
            response.getEntity().release();
         }
         throw new IOException("Cannot get entry media "+mediaRef+" due to status "+response.getStatus()+": "+msg);
      }
   }
   
   public Representation getMediaHead(String path,UUID feedId,String name) 
      throws IOException
   {
      Reference mediaRef = makeMediaReference(path,name);
      Response response = head(mediaRef);
      if (response.getStatus().isSuccess()) {
         return response.getEntity();
      } else {
         boolean hasEntity = response.isEntityAvailable();
         String msg = hasEntity ? response.getEntity().getText() : "";
         if (hasEntity) {
            response.getEntity().release();
         }
         throw new IOException("Cannot get entry media "+mediaRef+" due to status "+response.getStatus()+": "+msg);
      }
   }
   
   public boolean deleteMedia(String path,UUID feedId,String name)
   {
      Reference mediaRef = makeMediaReference(path,name);
      Response delResponse = delete(mediaRef);
      boolean success = delResponse.getStatus().isSuccess();
      boolean hasEntity = delResponse.isEntityAvailable();
      if (!success) {
         String msg = "";
         try {
            Representation rep = delResponse.getEntity();
            if (rep!=null) {
               msg = rep.getText();
            }
         } catch (IOException ex) {
         }
         getLogger().log(Level.SEVERE,"Cannot delete media "+mediaRef+": "+msg);
      }
      if (hasEntity) {
         delResponse.getEntity().release();
      }
      return success;
   }
   
   public Query getQuery(String path, UUID feedId, String name) 
      throws IOException
   {
      Reference mediaRef = makeMediaReference(path,name);
      Response response = head(mediaRef);
      if (response.getStatus().isSuccess()) {
         if (!response.getEntity().getMediaType().getName().equals("application/xquery")) {
            response.getEntity().release();
            throw new IOException("Media type "+response.getEntity().getMediaType().getName()+" on is not an XQuery.");
         }
         response.getEntity().release();
         return new XQuery(mediaRef);
      } else {
         if (response.isEntityAvailable()) {
            response.getEntity().release();
         }
         throw new IOException("Cannot retrieve media, status="+response.getStatus().getCode());
      }
      
   }
   
   public Query compileQuery(String query)
      throws IOException
   {
      return new XQuery(query);

   }

   public Representation queryFeed(String path,UUID feedId,Query query,Map<String,String> parameters)
      throws IOException
   {
      return internalQueryFeed(false,path,feedId,query,parameters);
   }
   
   public Representation queryCollection(String path,UUID feedId,Query query,Map<String,String> parameters)
      throws IOException
   {
      return internalQueryFeed(true,path,feedId,query,parameters);
   }
   
   Representation internalQueryFeed(boolean collection,String path,UUID feedId,Query query,Map<String,String> parameters)
      throws IOException
   {
      XQuery xquery = (XQuery)query;
      Reference feedRef = collection ? makeFeedCollectionReference(path) : makeFeedReference(path);
      if (parameters!=null) {
         StringBuilder refBuilder = new StringBuilder();
         refBuilder.append(feedRef.toString());
         refBuilder.append("?");
         boolean first = true;
         for (String name : parameters.keySet()) {
            String value = parameters.get(name);
            if (!first) {
               refBuilder.append("&");
            }
            refBuilder.append(name);
            refBuilder.append("=");
            refBuilder.append(URLEncoder.encode(value,"UTF-8"));
            first = false;
         }
         feedRef = new Reference(refBuilder.toString());
      }
      Response response = null;
      if (xquery.mediaRef!=null) {
         response = getWithQuery(feedRef,xquery.mediaRef);
      } else {
         response = post(feedRef,new StringRepresentation(xquery.query,AtomResource.XQUERY_TYPE));
      }
      if (response.getStatus().isSuccess()) {
         return response.getEntity();
      } else {
         String text = response.getStatus().getDescription();
         if (text==null) {
            text = "";
         }
         throw new IOException("Cannot query feed, status="+response.getStatus().getCode()+", "+text);
      }
   }
   
   
   public Reference makeEntryReference(String path,UUID entryId) {
      return new Reference(atomBase+"feeds/"+path+"."+entryId.toString()+".atom");
   }
   
   public Reference makeMediaReference(String path,String name) {
      try {
         return new Reference(atomBase+"feeds/"+path+URLEncoder.encode(name,"UTF-8"));
      } catch (UnsupportedEncodingException ex) {
         throw new RuntimeException("Encoding not supported.",ex);
      }
   }
   
   public Reference makeFeedReference(String path) {
      return new Reference(atomBase+"feeds/"+path+FEED_DOCUMENT_NAME);
   }
   
   public Reference makeFeedCollectionReference(String path) {
      return new Reference(atomBase+"feeds/"+path);
   }
}
